﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Threading;
using System.Xml.Linq;
using BathroomLocator.Phone.Models;
using Microsoft.Phone.Controls.Maps;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
using Microsoft.Samples.WindowsPhoneCloud.StorageClient;

namespace BathroomLocator.Phone.ViewModel
{
	public class BathroomLocationDetailsEditPageViewModel : BaseViewModel
	{
		private readonly ITableServiceContext context;
        private readonly ICloudBlobClient blobClient;
		private readonly Dispatcher dispatcher;
        private string message;
        private bool isSaving = false;
        private string blobName;
        private BathroomLocation bathroomLocation;

        private const string PhotoResultKey = "PhotoResult";

        public event EventHandler SaveChangesSuccess;
        public event EventHandler DeleteSuccess;

        private event EventHandler GeocodeAddressSuccess;
        private event EventHandler<PictureUploadEventArgs> PictureUploadSuccess;
        private event EventHandler<LocateUploadedBlobUriEventArgs> LocatedUploadedBlobUriSuccess;

		public BathroomLocationDetailsEditPageViewModel()
			: this(App.CloudClientFactory.ResolveTablesServiceContext("BathroomLocation"), 
                App.CloudClientFactory.ResolveCloudBlobClient("BathroomUploads"),
                Deployment.Current.Dispatcher)
		{
		}

		public BathroomLocationDetailsEditPageViewModel(ITableServiceContext context,
                                                        ICloudBlobClient blobClient,
                                                        Dispatcher dispatcher)
		{
			this.context = context;
            this.blobClient = blobClient;
			this.dispatcher = dispatcher;

            this.GeocodeAddressSuccess += new EventHandler(OnGeocodeAddressSuccess);
            this.PictureUploadSuccess += new EventHandler<PictureUploadEventArgs>(OnPictureUploadSuccess);
            this.LocatedUploadedBlobUriSuccess += new EventHandler<LocateUploadedBlobUriEventArgs>(OnLocatedUploadedBlobUriSuccess);
		}

		public BathroomLocation BathroomLocation
		{
			get { return this.bathroomLocation; }
			set
			{
				if (this.bathroomLocation != value)
				{
					this.bathroomLocation = value;
					this.NotifyPropertyChanged("BathroomLocation");
				}
			}
		}

		public string Message
		{
			get
			{
				return this.message;
			}

			set
			{
				if (this.message != value)
				{
					this.message = value;
					this.NotifyPropertyChanged("Message");
				}
			}
		}

		public bool IsSaving
		{
			get
			{
				return this.isSaving;
			}

			set
			{
				if (this.isSaving != value)
				{
					this.isSaving = value;
					this.NotifyPropertyChanged("IsSaving");
				}
			}
		}

		public void SaveData()
		{
			this.Message = "Saving your bathroom . . .";
			this.IsSaving = true;

            /*
             * Saving consists of three steps:
             * 1. Geocode the address
             * 2. Save the picture to Windows Azure blob storage
             * 3. Save the data to Windows Azure table storage
             */
			GeocodeAddress();
		}

        private string GetBingApiKey()
        {
            var key = App.Current.Resources["BingMapsAPIKey"] as ApplicationIdCredentialsProvider;
            if (key != null)
                return key.ApplicationId;

            return string.Empty;
        }

		private void GeocodeAddress()
		{
			UriBuilder uriBuilder = new UriBuilder("http://dev.virtualearth.net/REST/v1/Locations/");

            // TODO: Input data should be "cleaned" prior to using Bing API - remove trailing period (ex. 123 main st.) and spaces, etc.

			uriBuilder.Path += string.Format("{0}/{1}/{2}/{3}/{4}",
				"US",
				BathroomLocation.State,
				BathroomLocation.ZipCode,
				BathroomLocation.City,
				BathroomLocation.AddressLine1
				);

            // NOTE: Remove the 'o=xml' part of the query string to set the return format to JSON (default for the Bing Location API).
            var queryParams = string.Format("o=xml&key={0}", GetBingApiKey());
			uriBuilder.Query = queryParams;

		    var webClient = new WebClient();
		    webClient.DownloadStringCompleted += (sender, args) =>
		    {
                if (args.Error != null)
                {
                    this.Message = "Error geocoding your address.";
                    this.IsSaving = false;
                    return;
                }

		        var xmlResult = XElement.Parse(args.Result);

                XNamespace ns = "http://schemas.microsoft.com/search/local/ws/rest/v1";
		        var gps = from x in xmlResult.Descendants(ns + "Point")
		                  select new
		                             {
		                                 lat = x.Element(ns + "Latitude").Value,
		                                 lng = x.Element(ns + "Longitude").Value
		                             };

		        var g = gps.FirstOrDefault();
                if (g != null)
                {
                    BathroomLocation.GPSLatitude = g.lat;
                    BathroomLocation.GPSLongitude = g.lng;
                }

                RaiseGeocodeAddressSuccess();
		    };
            webClient.DownloadStringAsync((uriBuilder.Uri));
		}

		public void DeleteData()
		{
            this.Message = "Deleting . . .";
            this.IsSaving = true;

            // TODO: Add support for deleting associated blob data (the image).

            this.BathroomLocation.PropertyChanged -= this.OnUpdateData;
            this.context.DeleteObject(this.BathroomLocation);
            this.context.BeginSaveChanges(this.OnBeginDelete, null);
		}

		public void SetBathroomLocationModel(BathroomLocation bathroom = null)
		{
			if (bathroom == null)
			{
				this.BathroomLocation = new BathroomLocation();
				this.context.AddObject("BathroomLocation", this.BathroomLocation);
			}
			else
			{
				this.BathroomLocation = bathroom;
				this.BathroomLocation.PropertyChanged += this.OnUpdateData;
			}
		}

        public void SavePictureToPhone(PhotoResult result)
        {
            if (PhoneApplicationService.Current.State.ContainsKey(PhotoResultKey))
            {
                PhoneApplicationService.Current.State.Remove(PhotoResultKey);
            }

            PhoneApplicationService.Current.State.Add(PhotoResultKey, result);
        }

        private static T GetApplicationState<T>(string key)
        {
            if (!PhoneApplicationService.Current.State.ContainsKey(key))
            {
                return default(T);
            }

            return (T)PhoneApplicationService.Current.State[key];
        }

        private void UploadPicture(Action<string> successCallback, Action<string> failureCallback, PhotoResult photoResult)
        {
            Stream photoStream = photoResult.ChosenPhoto;
            this.blobName = string.Format("bathroom-{0}{1}",
                DateTime.UtcNow.ToString("yyyyMMddTHHmmss"),
                System.IO.Path.GetExtension(photoResult.OriginalFileName));

            this.blobClient.Upload(
                this.blobName,
                photoStream,
                cb => this.dispatcher.BeginInvoke(
                    () =>
                    {
                        if (cb.Exception == null)
                        {
                            if (successCallback != null)
                            {
                                successCallback.Invoke(string.Format("Picture {0} uploaded.", this.blobName));
                            }
                        }
                        else
                        {
                            if (failureCallback != null)
                                failureCallback.Invoke(string.Format("Error uploading picture: {0}", cb.Exception.Message));
                        }
                    }));
        }

        private void OnGeocodeAddressSuccess(object sender, EventArgs e)
        {
            var photoResult = GetApplicationState<PhotoResult>("PhotoResult");
            if (photoResult != null)
            {
                UploadPicture(
                    (successMessage) =>
                    {
                        this.dispatcher.BeginInvoke(() =>
                        {
                            // Remove the picture from the phone and save the entity.
                            PhoneApplicationService.Current.State.Remove(PhotoResultKey);

                            // Raise event to indicate the picture has been uploaded successfully.
                            RaisePictureUploadSuccess();
                        });
                    },
                    (errorMessage) =>
                    {
                        this.dispatcher.BeginInvoke(() =>
                        {
                            this.Message = string.Format("Failed to save picture. {0}", errorMessage);
                            this.IsSaving = false;
                        });
                    },
                    photoResult);
            }
            else
            {
                this.context.BeginSaveChanges(this.OnBeginSaveChanges, null);
            }
        }

        private void OnUpdateData(object sender, PropertyChangedEventArgs e)
        {
            this.BathroomLocation.PropertyChanged -= this.OnUpdateData;
            this.context.UpdateObject(this.BathroomLocation);
        }

        private void OnBeginDelete(IAsyncResult asyncResult)
        {
            this.dispatcher.BeginInvoke(
                () =>
                {
                    try
                    {
                        this.context.EndSaveChanges(asyncResult);

                        this.Message = "Bathroom deleted successfully.";
                        this.IsSaving = false;

                        this.RaiseDeleteSuccess();
                    }
                    catch (Exception exception)
                    {
                        this.Message = string.Format(
                            CultureInfo.InvariantCulture,
                            "Error: {0}",
                            StorageClientExceptionParser.ParseDataServiceException(exception).Message);

                        this.IsSaving = false;
                    }
                });
        }

        private void OnBeginSaveChanges(IAsyncResult asyncResult)
        {
            this.dispatcher.BeginInvoke(
                () =>
                {
                    try
                    {
                        this.context.EndSaveChanges(asyncResult);

                        this.Message = "Changes saved successfully.";
                        this.IsSaving = false;

                        this.RaiseSaveChangesSuccess();
                    }
                    catch (Exception exception)
                    {
                        this.Message = string.Format(
                            CultureInfo.InvariantCulture,
                            "Error: {0}",
                            StorageClientExceptionParser.ParseDataServiceException(exception).Message);

                        this.IsSaving = false;
                    }
                });
        }

        private void OnLocatedUploadedBlobUriSuccess(object sender, LocateUploadedBlobUriEventArgs e)
        {
            this.dispatcher.BeginInvoke(() =>
            {
                this.BathroomLocation.ImagePath = e.FullUri;

                this.context.BeginSaveChanges(this.OnBeginSaveChanges, null);
            });
        }

        private void OnPictureUploadSuccess(object sender, PictureUploadEventArgs e)
        {
            // Attempt to find the picture that was just uploaded to get the full blob URI.
            this.blobClient.ListBlobsWithPrefix(this.blobName, true, (results) =>
            {
                // Should only be one in the result.
                if (results.Response.Count() == 1)
                {
                    // Result has a shared access signature attached - remove it.
                    var fullUri = results.Response[0].Uri.ToString();
                    fullUri = fullUri.Substring(0, fullUri.IndexOf('?'));

                    if (this.LocatedUploadedBlobUriSuccess != null)
                        LocatedUploadedBlobUriSuccess(this, new LocateUploadedBlobUriEventArgs { FullUri = fullUri });
                }
            });
        }

        private void RaiseDeleteSuccess()
        {
            if (this.DeleteSuccess != null)
                DeleteSuccess(this, EventArgs.Empty);
        }

        private void RaiseSaveChangesSuccess()
        {
            var saveChangesSuccess = this.SaveChangesSuccess;
            if (saveChangesSuccess != null)
            {
                saveChangesSuccess(this, EventArgs.Empty);
            }
        }

        private void RaisePictureUploadSuccess()
        {
            if (PictureUploadSuccess != null)
                PictureUploadSuccess(this, new PictureUploadEventArgs { BlobName = blobName });
        }

        private void RaiseGeocodeAddressSuccess()
        {
            if (this.GeocodeAddressSuccess != null)
                GeocodeAddressSuccess(this, EventArgs.Empty);
        }
    }

    public class PictureUploadEventArgs : EventArgs
    {
        public string BlobName { get; set; }
    }

    public class LocateUploadedBlobUriEventArgs : EventArgs
    {
        public string FullUri { get; set; }
    }
}
